package com.bango.nokia.java;

import java.io.ByteArrayInputStream;
import java.io.IOException;
// import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import javax.microedition.io.Connector;
import javax.microedition.io.HttpConnection;
import javax.microedition.location.Criteria;
import javax.microedition.location.Location;
import javax.microedition.location.LocationException;
import javax.microedition.location.LocationListener;
import javax.microedition.location.LocationProvider;
import javax.microedition.rms.RecordEnumeration;

class BangoLogger implements LocationListener
{

	private BangoDatabaseStorage _store = new BangoDatabaseStorage();

	private LocationProvider _locationProvider;

	private String _applicationId;
	private String _userAgent;
	private String _userId;

	private BangoProfile _bangoProfile;
	private Vector _bangoEvents;

	private boolean _logEvents = BangoConstants.SETTING_LOGEVENTS;
	private int _sessionResumePeriod = BangoConstants.SETTING_SESSION_RESUME_PERIOD;
	private String _postURL = BangoConstants.SETTING_POST_URL;
	private String _locationSource = BangoConstants.SETTING_LOCATION_SOURCE;
	private boolean _findLocation = BangoConstants.SETTING_LOCATION_FIND;
	private boolean _useOfflineStorage = BangoConstants.SETTING_USEOFFLINESTORAGE;
	private String _applicationType = "";
	private String _applicationChannel = "";
	private String _applicationVersion;

	BangoProfile Profile()
	{
		return _bangoProfile;
	}

	Vector Events()
	{
		return _bangoEvents;
	}

	void LogEvents(boolean logEvents)
	{
		_logEvents = logEvents;
	}

	boolean LogEvents()
	{
		return _logEvents;
	}

	void SessionResumePeriod(int sessionResumePeriod)
	{
		_sessionResumePeriod = sessionResumePeriod;
	}

	int SessionResumePeriod()
	{
		return _sessionResumePeriod;
	}

	void ApplicationVersion(String applicationVersion)
	{
		_applicationVersion = applicationVersion;
	}

	String ApplicationVersion()
	{
		return _applicationVersion;
	}

	void ApplicationType(String applicationType)
	{
		_applicationType = applicationType;
	}

	String ApplicationType()
	{
		return _applicationType;
	}

	void ApplicationChannel(String applicationChannel)
	{
		_applicationChannel = applicationChannel;
	}

	String ApplicationChannel()
	{
		return _applicationChannel;
	}

	void UserId(String userId)
	{
		_userId = userId;
	}

	String UserId()
	{
		return _userId;
	}

	void UserAgent(String userAgent)
	{
		_userAgent = userAgent;
	}

	String UserAgent()
	{
		return _userAgent;
	}

	void SetPostURL(String postURL)
	{
		_postURL = postURL;
	}

	String PostURL()
	{
		return _postURL;
	}

	void LocationSource(String locationSource)
	{

		_locationSource = locationSource;
	}

	String LocationSource()
	{
		return _locationSource;
	}

	void FindLocation(boolean findLocation)
	{
		_findLocation = findLocation;
	}

	boolean FindLocation()
	{
		return _findLocation;
	}

	void UseOfflineStorage(boolean useOfflineStorage)
	{
		_useOfflineStorage = useOfflineStorage;
	}

	boolean UseOfflineStorage()
	{
		return _useOfflineStorage;
	}

	BangoLogger()
	{

	}

	/**
	 * Initialises the logging system. This should be called when starting the
	 * application, and is normally done with the onStartSession call.
	 * 
	 * @param userAgent
	 *            The user agent of the device.
	 * @param applicationId
	 *            The application Id of the application. This should be setup in
	 *            the Bango Management Tools prior to use.
	 * @param applicationVersion
	 *            The version of the application.
	 */
	void InitLogger(String applicationId, String applicationVersion)
	{
		String locationSource = "";
		double locationLatitude = 0;
		double locationLongitude = 0;
		float locationAltitude = 0;
		float locationAccuracy = 0;

		String networkName;
		String networkCountry;
		String networkId;

		_applicationId = applicationId;
		_applicationVersion = applicationVersion;

		if ((_userAgent == null) || (_userAgent == ""))
		{
			_userAgent = BangoStatic.GetUserAgent();
		}

		if ((_userId == null) || (_userId == ""))
		{
			_userId = BangoStatic.GetUserID();
		}

		networkName = BangoStatic.GetProperty(BangoConstants.ACCESS_NETWORK_NAME);
		if (networkName.equals(""))
		{
			networkName = BangoConstants.UNKNOWN_VERSION;
		}

		networkCountry = BangoStatic.GetCountryCode();

		networkId = BangoStatic.GetProperty(BangoConstants.ACCESS_NETWORK_ID);
		if (networkId.equals(""))
		{
			networkId = BangoConstants.UNKNOWN_VERSION;
		}

		try
		{
			Location deviceLocation = GetLocation();

			if (deviceLocation != null && deviceLocation.isValid())
			{
				if (_locationSource.equalsIgnoreCase(BangoConstants.LOCATION_CELL))
				{
					locationSource = BangoConstants.LOCATION_CELL;
				}
				else if (_locationSource.equalsIgnoreCase(BangoConstants.LOCATION_ASSISTED))
				{
					locationSource = BangoConstants.LOCATION_ASSISTED;
				}
				else
				{
					locationSource = BangoConstants.LOCATION_GPS;
				}

				locationAccuracy = 0;
				locationAltitude = 0;

				locationLatitude = deviceLocation.getQualifiedCoordinates().getLatitude();
				locationLongitude = deviceLocation.getQualifiedCoordinates().getLongitude();

				if (!Float.isNaN(deviceLocation.getQualifiedCoordinates().getAltitude()))
				{
					locationAltitude = deviceLocation.getQualifiedCoordinates().getAltitude();
				}

				if (!Float.isNaN(deviceLocation.getQualifiedCoordinates().getHorizontalAccuracy()))
				{
					locationAccuracy = deviceLocation.getQualifiedCoordinates().getHorizontalAccuracy();
				}
			}
		}
		catch (Exception ex)
		{

		}

		_bangoProfile = new BangoProfile(BangoConstants.SDK_TYPE, BangoConstants.SDK_VERSION, _applicationId, _applicationType, _applicationChannel, _applicationVersion, _userId, _sessionResumePeriod, ConnectionType(), IsRoaming(), locationSource, locationAltitude, locationLatitude, locationLongitude, locationAccuracy, networkId, networkName, networkCountry, networkName, BangoSymbianDeviceDetails.DeviceMake(), BangoSymbianDeviceDetails.DeviceModelAndVersion(), BangoSymbianDeviceDetails.FirmwareVersion(), BangoSymbianDeviceDetails.PlatformName(), BangoSymbianDeviceDetails.PlatformVersion(), BangoSymbianDeviceDetails.JavaRuntimeVersion(), BangoStatic.GetOperatorConnectionType());

		_bangoEvents = new Vector();
	}

	/**
	 * Posts any events in the persistent store.
	 * 
	 * @return Whether or not at least 1 profile and associated events were
	 *         posted successfully.
	 */
	synchronized boolean PostStoredEvents()
	{
		boolean returnValue = false;
		String strJSON;
		int recordId = 0;

		_store.OpenRecordStore();

		RecordEnumeration re;

		try
		{
			re = _store.GetRecordEnumeration();

			while (re.hasNextElement())
			{
				recordId = re.nextRecordId();

				strJSON = _store.ReadRecord(recordId);

				if (PostEvents(strJSON))
				{
					_store.DeleteRecord(recordId);
					returnValue = true;
				}
			}
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
		finally
		{
			_store.CloseRecordStore();
		}

		return returnValue;
	}

	/**
	 * Posts the current profile and associated events.
	 * 
	 * @return Whether or not the profile and associated events were posted
	 *         successfully.
	 */
	synchronized boolean PostEvents()
	{
		boolean returnValue = false;
		Vector eventsToPost;

		if (_bangoEvents.size() > 0)
		{
			eventsToPost = new Vector();

			for (int appEventCounter = 0; appEventCounter < _bangoEvents.size(); appEventCounter++)
			{
				if (!((BangoEvent) _bangoEvents.elementAt(appEventCounter)).EventPosted())
				{
					eventsToPost.addElement(((BangoEvent) _bangoEvents.elementAt(appEventCounter)));
				}
			}

			if (eventsToPost.size() > 0)
			{
				returnValue = PostEvents(BangoStatic.GetJSONString(_bangoProfile, eventsToPost));
				if (returnValue)
				{
					// Set all events as posted
					for (int appEventCounter = 0; appEventCounter < eventsToPost.size(); appEventCounter++)
					{
						((BangoEvent) eventsToPost.elementAt(appEventCounter)).EventPosted(true);
					}
				}
			}
		}

		return returnValue;
	}

	/**
	 * Posts the supplied profile and associated events to the specified url.
	 * 
	 * @param strJSON
	 *            Batch represented as JSON String.
	 * @return Whether or not the profile and associated events were posted
	 *         successfully.
	 */
	private synchronized boolean PostEvents(String jsonString)
	{
		boolean returnValue = false;
		// String responseMessage;
		int responseCode;
		final int BUFFER_SIZE = 1024;
		HttpConnection httpPost = null;
		OutputStream outputStream = null;
		// InputStream inputStream = null;
		ByteArrayInputStream byteOutputStream = null;
		byte[] outputBuffer = new byte[BUFFER_SIZE];
		int jsonLength;
		int inputInt;

		/*
		 * There is an issue where null bytes are being added to the end of the
		 * stream. To get rid of these, the string is padded to an exact
		 * multiple of the buffer size
		 */
		jsonLength = jsonString.getBytes().length;
		jsonLength = jsonLength + BUFFER_SIZE - (jsonLength % BUFFER_SIZE);
		jsonString = BangoStatic.PadRight(jsonString, ' ', jsonLength);

		try
		{
			if (_logEvents && (!IsRoaming() || IsWifiConnection()))
			{
				httpPost = (HttpConnection) Connector.open(_postURL);

				httpPost.setRequestMethod(HttpConnection.POST);
				if (_userAgent != null && _userAgent != "")
				{
					httpPost.setRequestProperty("User-Agent", _userAgent);
				}
				httpPost.setRequestProperty("Content-Type", "application/json");
				httpPost.setRequestProperty("Content-Length", Integer.toString(jsonLength));
				httpPost.setRequestProperty("Connection", "close");

				outputStream = httpPost.openOutputStream();

				byteOutputStream = new ByteArrayInputStream(jsonString.getBytes());

				while ((inputInt = byteOutputStream.read(outputBuffer)) > 0)
				{
					outputStream.write(outputBuffer);
				}

				outputStream.flush();
				outputStream.close();

				/*
				 * inputStream = httpPost.openInputStream(); responseMessage =
				 * ""; while ((inputInt = inputStream.read()) != -1) {
				 * responseMessage += ((char) inputInt); }
				 */

				responseCode = httpPost.getResponseCode();

				if (responseCode == 200)
				{
					returnValue = true;
				}
			}
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
			returnValue = false;
		}
		finally
		{
			try
			{
				if (byteOutputStream != null)
				{
					byteOutputStream.close();
				}

				if (outputStream != null)
				{
					outputStream.close();
				}

				if (httpPost != null)
				{
					httpPost.close();
				}
			}
			catch (IOException e)
			{
			}
		}

		return returnValue;
	}

	/**
	 * Gets the connection type. If the connection is a WiFi connection this
	 * will be returned. Otherwise the operator connection type will be
	 * returned.
	 * 
	 * @return The connection type in integer value. This is converted to a
	 *         textual value on posting.
	 */
	private int ConnectionType()
	{
		String connType = BangoStatic.GetProperty(BangoConstants.ACCESS_NETWORK_ACCESS_TYPE);
		int rtnVal;

		if (connType.equalsIgnoreCase("wlan") || connType.equalsIgnoreCase("lan") || connType.equalsIgnoreCase("bt_pan"))
		{
			rtnVal = BangoConstants.WIFI_CONNECTION;
		}
		else if (connType.equalsIgnoreCase("na"))
		{
			rtnVal = BangoConstants.UNKWNOWN_CONNECTION;
		}
		else
		{
			rtnVal = BangoConstants.OPERATOR_CONNECTION;
		}
		return rtnVal;
	}

	/**
	 * Gets whether or not the user is currently roaming.
	 * 
	 * @return Whether or not the user is currently roaming.
	 */
	private boolean IsRoaming()
	{
		return (BangoStatic.GetProperty(BangoConstants.ACCESS_NETWORK_ROAMING_STATUS).equalsIgnoreCase("roaming"));
	}

	/**
	 * Gets whether or not the user is currently using a WiFi connection.
	 * 
	 * @return Whether or not the user is currently using a WiFi connection.
	 */
	private boolean IsWifiConnection()
	{
		return (ConnectionType() == BangoConstants.WIFI_CONNECTION);
	}

	/**
	 * Saves the persistent store. This should be called when exiting the
	 * application, and is normally done with the onEndSession call.
	 */
	synchronized boolean SaveStore()
	{
		_store.OpenRecordStore();

		Vector eventsToPost = new Vector();

		for (int appEventCounter = 0; appEventCounter < _bangoEvents.size(); appEventCounter++)
		{
			if (!((BangoEvent) _bangoEvents.elementAt(appEventCounter)).EventPosted())
			{
				eventsToPost.addElement(((BangoEvent) _bangoEvents.elementAt(appEventCounter)));
			}
		}

		if (_store.WriteRecord(BangoStatic.GetJSONString(_bangoProfile, eventsToPost)))
		{
			_bangoEvents = new Vector();
			return true;
		}
		else
		{
			return false;
		}
	}

	/**
	 * Logs an event to the internal logging engine.
	 * 
	 * @param eventName
	 *            The name of the event to log.
	 * @param errorLevel
	 *            The error level of the event to log.
	 * @param eventDetail
	 *            The detail of the event to log.
	 * @param eventValue
	 *            The value of the event to log.
	 * @param errorId
	 *            The error Id of the event to log.
	 * @param eventParameters
	 *            Any custom parameters to log with events.
	 * @return the internal id of the event.
	 */
	private int LogEvent(String eventName, int errorLevel, String eventValue, String eventDetail, String errorId, Hashtable eventParameters)
	{
		int returnValue = -1;
		Date eventLocalTime = new Date();
		Date eventTime = BangoStatic.GetUTCDate(eventLocalTime);
		Vector eventParametersVector = null;
		String eventParameterName;
		if (eventParameters != null)
		{
			eventParametersVector = new Vector();
			for (Enumeration eventParameterEnumerator = eventParameters.keys(); eventParameterEnumerator.hasMoreElements();)
			{
				eventParameterName = (String) eventParameterEnumerator.nextElement();
				eventParametersVector.addElement(new BangoNameValuePair(eventParameterName, eventParameters.get(eventParameterName).toString()));
			}
		}

		if (_logEvents)
		{
			_bangoEvents.addElement(new BangoEvent(_bangoEvents.size(), ConnectionType(), BangoStatic.GetOperatorConnectionType(), errorLevel, eventName, eventValue, eventDetail, errorId, eventTime, eventLocalTime, eventParametersVector));
			returnValue = _bangoEvents.size();
		}
		return returnValue;
	}

	/**
	 * Logs an event to the internal logging engine.
	 * 
	 * @param eventName
	 *            The name of the event to log.
	 * @param eventDetail
	 *            The detail of the event to log.
	 * @param eventValue
	 *            The value of the event to log.
	 * @param eventParameters
	 *            Any custom parameters to log with events.
	 * @return The internal id of the event.
	 */
	int LogEvent(String eventName, String eventValue, String eventDetail, Hashtable eventParameters)
	{
		return LogEvent(eventName, BangoConstants.ERROR_NONE, eventValue, eventDetail, "", eventParameters);
	}

	/**
	 * Logs an the session start event to the internal logging engine.
	 * 
	 * @param eventDetail
	 *            The detail of the event to log.
	 * @param eventValue
	 *            The value of the event to log.
	 * @param eventParameters
	 *            Any custom parameters to log with events.
	 * @return The internal id of the event.
	 */
	int LogEventStart(String eventValue, String eventDetail, Hashtable eventParameters)
	{
		return LogEvent(BangoSessionEvents.SESSION_START, eventValue, eventDetail, eventParameters);
	}

	/**
	 * Logs an the session end event to the internal logging engine.
	 * 
	 * @param eventDetail
	 *            The detail of the event to log.
	 * @param eventValue
	 *            The value of the event to log.
	 * @param eventParameters
	 *            Any custom parameters to log with events.
	 * @return The internal id of the event.
	 */
	int LogEventEnd(String eventValue, String eventDetail, Hashtable eventParameters)
	{
		return LogEvent(BangoSessionEvents.SESSION_END, eventValue, eventDetail, eventParameters);
	}

	/**
	 * Logs an error event to the internal logging engine.
	 * 
	 * @param errorId
	 *            The error Id of the event to log.
	 * @param message
	 *            The error message.
	 * @param errorClass
	 *            The class in which the error occurred.
	 * @param eventParameters
	 *            Any custom parameters to log with events.
	 * @return The internal id of the error.
	 */
	int LogEventError(String errorId, String message, String errorClass, Hashtable eventParameters)
	{
		return LogEvent(BangoSessionEvents.EVENT_ERROR, BangoConstants.ERROR_USER, errorClass, message, errorId, eventParameters);
	}

	/**
	 * Logs a critical internal error event to the internal logging engine.
	 * 
	 * @param ex
	 *            The error that was raised.
	 * @return The internal id of the error.
	 */
	int LogInternalErrorCritical(Exception ex)
	{
		return LogInternalError(ex, BangoConstants.ERROR_INTERNAL_CRITICAL);
	}

	/**
	 * Logs a warning internal error event to the internal logging engine.
	 * 
	 * @param ex
	 *            The error that was raised.
	 * @return The internal id of the error.
	 */
	int LogInternalErrorWarning(Exception ex)
	{
		return LogInternalError(ex, BangoConstants.ERROR_INTERNAL_WARNING);
	}

	/**
	 * Logs an internal error event to the internal logging engine.
	 * 
	 * @param ex
	 *            The error that was raised.
	 * @param errorLevel
	 *            The error level of the error (Warning/Critical)
	 * @return The internal id of the error.
	 */
	private int LogInternalError(Exception ex, int errorLevel)
	{
		return LogEvent("", errorLevel, ex.toString(), ex.getMessage(), "", null);
	}
	
	/**
	 * 
	 * @return The JSON String representing the Current Batch.
	 */
	
	public String viewBatch()
	{
		return BangoStatic.GetJSONString(_bangoProfile, _bangoEvents);
	}
		

	/**
	 * Initialises the chosen location provider.
	 * 
	 * @return The last known location. If this is not known known null is
	 *         returned.
	 */
	private Location GetLocation()
	{
		Location returnValue = null;
		Criteria criteria;
		if (_findLocation)
		{
			criteria = new Criteria();

			if (_locationSource == BangoConstants.LOCATION_CELL)
			{
				criteria.setPreferredPowerConsumption(Criteria.POWER_USAGE_LOW);
				criteria.setHorizontalAccuracy(Criteria.NO_REQUIREMENT);
				criteria.setVerticalAccuracy(Criteria.NO_REQUIREMENT);
				criteria.setCostAllowed(true);
			}
			else if (_locationSource == BangoConstants.LOCATION_ASSISTED)
			{
				criteria.setPreferredPowerConsumption(Criteria.POWER_USAGE_MEDIUM);
				criteria.setHorizontalAccuracy(500);
				criteria.setVerticalAccuracy(500);
				criteria.setCostAllowed(true);
			}
			else
			{
				criteria = new Criteria();
				criteria.setPreferredPowerConsumption(Criteria.POWER_USAGE_HIGH);
				criteria.setHorizontalAccuracy(2000);
				criteria.setVerticalAccuracy(2000);
				criteria.setCostAllowed(false);
			}

			try
			{
				_locationProvider = LocationProvider.getInstance(criteria);
			}
			catch (LocationException ex)
			{
				_findLocation = false;
			}

			if (_locationProvider != null)
			{
				try
				{
					returnValue = LocationProvider.getLastKnownLocation();
					EndLocationUpdates();
				}
				catch (Exception ex)
				{
					_findLocation = false;
				}
			}
		}
		return returnValue;
	}

	/**
	 * Destroys the location finder.
	 */
	void EndLocationUpdates()
	{
		if (_findLocation && _locationProvider != null)
		{
			_locationProvider.reset();
			_locationProvider = null;
		}
	}

	/**
	 * Updates the profile with the new location.
	 * 
	 * @param location
	 *            The new location of the user.
	 */
	private void UpdateLocation(Location location)
	{
		if (location.isValid())
		{
			float altitude = 0;
			float accuracy = 0;
			if (!Float.isNaN(location.getQualifiedCoordinates().getAltitude()))
			{
				altitude = location.getQualifiedCoordinates().getAltitude();
			}
			if (!Float.isNaN(location.getQualifiedCoordinates().getHorizontalAccuracy()))
			{
				accuracy = location.getQualifiedCoordinates().getHorizontalAccuracy();
			}

			EndLocationUpdates();

			_bangoProfile.LocationLatitude(location.getQualifiedCoordinates().getLatitude());
			_bangoProfile.LocationLongitude(location.getQualifiedCoordinates().getLongitude());
			_bangoProfile.LocationAccuracy(accuracy);
			_bangoProfile.LocationAltitude(altitude);
		}
	}

	/**
	 * Called when the location provider has been updated.
	 * 
	 * @param provider
	 *            The provider used to obtain the latest location.
	 * @param location
	 *            The new location of the user.
	 */
	public void locationUpdated(LocationProvider provider, Location location)
	{
		UpdateLocation(location);
	}

	/**
	 * Called by the LocationProvider to which this listener is registered if
	 * the state of the LocationProvider has changed.
	 * 
	 * @param provider
	 *            The provider used to obtain the latest location.
	 * @param newState
	 *            The new state of the LocationProvider. This value is one of
	 *            the constants for the state defined in the LocationProvider
	 *            class.
	 */
	public void providerStateChanged(LocationProvider provider, int newState)
	{
	}
}
